/*
 * InputCandidate.java
 *
 * Created on Dec 22, 2011
 *
 * Copyright (c) 2011 Artur Rataj.
 *
 * This code is distributed under the terms of the GNU Library
 * General Public License, either version 3 of the license or, at
 * your option, any later version.
 */

package pl.gliwice.iitis.spatialnn.network.construct;

import java.util.*;

import pl.gliwice.iitis.graphutils.Point;
import pl.gliwice.iitis.graphutils.Rect;
import pl.gliwice.iitis.spatialnn.network.AbstractTarget;

/**
 * An input candidate. The location can be corrected when clustering.
 * 
 * @author Artur Rataj
 */
public class InputCandidate {
    /**
     * Where this input candidate outputs. Can be null if a candidate for a start input for the turn
     * zone.
     */
    public AbstractTarget target;
    /**
     * Current location of this input.
     */
    public Point loc;
    /**
     * Type of the input, null for unspecified.
     */
    public InputType type;
    
    /**
     * Creates a new instance of <code>InputCandidate</code>.
     * 
     * @param target where this input candidate outputs
     * @param loc primary location of this input
     * @param type type of the input, null for unspecified
     */
    public InputCandidate(AbstractTarget target, Point loc, InputType type) {
        this.target = target;
        this.loc = loc;
        this.type = type;
    }
    /**
     * Returns a rectangle, that is a bound of a list of input candidates.
     * 
     * @param inputs input candidates
     * @return a rectangle representing the bound
     */
    public static Rect getBound(List<InputCandidate> inputs) {
        Point min = new Point(Double.MAX_VALUE, Double.MAX_VALUE);
        Point max = new Point(-Double.MAX_VALUE, -Double.MAX_VALUE);
        for(InputCandidate c : inputs) {
            if(min.x > c.loc.x)
                min.x = c.loc.x;
            if(min.y > c.loc.y)
                min.y = c.loc.y;
            if(max.x < c.loc.x)
                max.x = c.loc.x;
            if(max.y < c.loc.y)
                max.y = c.loc.y;
        }
        return new Rect(min, max);
    }
    /**
     * Returns the ratio of candidates, that lie within a bound.
     * 
     * @param inputs input candidates
     * @param bound a bound
     * @return ratio, 0.0 ... 1.0
     */
    public static double getRatioIn(List<InputCandidate> inputs, Rect bound) {
        final double EPSILON = 1e-6;
        int count = 0;
        for(InputCandidate c : inputs)
            if(
                    c.loc.x + EPSILON >= bound.min.x && c.loc.x - EPSILON <= bound.max.x &&
                    c.loc.y + EPSILON >= bound.min.y && c.loc.y - EPSILON <= bound.max.y)
                ++count;
            else
                c = c;
        return count*1.0/inputs.size();
    }
    /**
     * Shrinks or grows a bound.
     * 
     * @param bound bound to modify
     * @param mean precomputed middle point of the bound
     * @param scale &gt; 1 for grow, &lt; 1 for shrink
     */
    protected static void changeBound(Rect bound, Point mean, double scale) {
        bound.min.x = mean.x + scale*(bound.min.x - mean.x);
        bound.min.y = mean.y + scale*(bound.min.y - mean.y);
        bound.max.x = mean.x + scale*(bound.max.x - mean.x);
        bound.max.y = mean.y + scale*(bound.max.y - mean.y);
    }
    /**
     * Returns a rectangle, that contains a given percent of input
     * candidates.
     * 
     * @param inputs input candidates
     * @param ratio ratio of the candidates to be included in the bound
     * @return a rectangle representing the tightened bound
     */
    public static Rect getTightenedBound(List<InputCandidate> inputs, double ratio) {
        Point mean = new Point(0.0, 0.0);
        for(InputCandidate c : inputs) {
            mean.x += c.loc.x;
            mean.y += c.loc.y;
            if(c.loc.x > 1.58)
                c = c;
        }
        mean.x = mean.x/inputs.size();
        mean.y = mean.y/inputs.size();
        Rect bound = getBound(inputs);
        if(ratio >= 0.999)
                return bound;
        Rect boundOrig = new Rect(new Point(bound.min), new Point(bound.max));
        Point boundMean = new Point((bound.min.x + bound.max.x)/2.0,
                (bound.min.y + bound.max.y)/2.0);
        Point offset = new Point(boundMean.x - mean.x,
                boundMean.y - mean.y);
        bound.min.x -= offset.x;
        bound.min.y -= offset.y;
        bound.max.x -= offset.x;
        bound.max.y -= offset.y;
        double ratioIn = getRatioIn(inputs, bound);
        double step = 0.2;
        while(Math.abs(ratioIn - ratio) > 1e-2 && step > 0.02) {
            if(ratioIn > ratio)
                // shrink
                changeBound(bound, mean, 1.0 - step);
            else
                changeBound(bound, mean, 1.0 + step);
            step *= 0.75;
            ratioIn = getRatioIn(inputs, bound);
        } 
        while(ratioIn == 1.0) {
            changeBound(bound, mean, 0.99);
            ratioIn = getRatioIn(inputs, bound);
        } 
        changeBound(bound, mean, 1.01);
        if(bound.min.x < boundOrig.min.x)
            bound.min.x = boundOrig.min.x;
        if(bound.min.y < boundOrig.min.y)
            bound.min.y = boundOrig.min.y;
        if(bound.max.x > boundOrig.max.x)
            bound.max.x = boundOrig.max.x;
        if(bound.max.y > boundOrig.max.y)
            bound.max.y = boundOrig.max.y;
        return bound;
    }
}
